package in.co.codedoc.sql;

import in.co.codedoc.cg.annotations.DBType;
import in.co.codedoc.cg.annotations.IsAnIOCComponent;
import in.co.codedoc.ioc.IOCContainer;
import in.co.codedoc.sql.mapping.BigintLongOutputColumnMap;
import in.co.codedoc.sql.mapping.SmallintShortOutputColumnMap;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.HashMap;

@IsAnIOCComponent
public class SequenceGenerator
{
	public synchronized long GetNextValue(String sequenceName)
	{
		Logger.logger.debug("Seqeunces:" + sequenceByName);
		SequenceState seq = sequenceByName.get(sequenceName);
		
		if((seq == null) || (seq != null && seq.AtEnd()))
		{
			sequenceByName.put(sequenceName,seq = CreateSequence(sequenceName));
			Logger.logger.debug("AFTER Seqeunces:" + sequenceByName);
		}
		
		return seq.Increment();
	}
	
	private SequenceState CreateSequence(String sequenceName)
	{
		DBInteractions.StartNewTransaction();
		try
		{
			SequenceState ss = CreateSequence(sequenceName, 0); 
			DBInteractions.CommitTransaction();
			
			return ss;
		}
		catch(Throwable th)
		{
			DBInteractions.RollbackTransaction();
			
			throw new RuntimeException(th);
		}
		
	}
	
	private SequenceState CreateSequence(String sequenceName,int depth)
		throws SQLException
	{
		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug("(Trial " + depth + ")Looking for Seqeunce " + sequenceName);
		}
		
		if(depth >= 100)
		{
			throw new RuntimeException("CreateSequence(" + sequenceName + "): Depth exceeded 100. Giving up.");
		}
		
		SequenceState ss = new SequenceState(sequenceName);

		if (GetSequenceState(ss))
		{
			if (Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("(Trial " + depth + ")FOUND Seqeunce "
						+ sequenceName + ". Grabbing next block.");
			}

			UpdateStatement updateStatement = new UpdateStatement();

			updateStatement.Set(SequenceColumns.NextValue,
					ss.nextValue + ss.blockSize).Set(SequenceColumns.Version,
					ss.version + 1).Where().And(SequenceColumns.Name, ss.name)
					.And(SequenceColumns.Version, ss.version);

			if (updateStatement.ExecuteUpdate())
			{
				ss.version++;
				ss.nextValue += ss.blockSize;
				ss.current = ss.nextValue;
			}
			else
			{
				ss = CreateSequence(sequenceName, depth + 1);
			}
		}
		else
		{
			if (Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("(Trial " + depth + ")Seqeunce "
						+ sequenceName + " NOT DEFINED. Defining.");
			}

			InsertStatement insert = new InsertStatement();

			insert.Set(SequenceColumns.Name, ss.name).Set(
					SequenceColumns.Version, ss.version).Set(
					SequenceColumns.BlockSize, ss.blockSize).Set(
					SequenceColumns.NextValue, ss.nextValue);

			if (!insert.ExecuteUpdate())
			{
				ss = CreateSequence(sequenceName, depth + 1);
			}
		}

		return ss;
	}

	private boolean GetSequenceState(SequenceState ss) 
		throws SQLException
	{
		QueryStatement query = new QueryStatement();
		
		query
			.Select(SequenceColumns.Name)
			.Select(SequenceColumns.Version)
			.Select(SequenceColumns.NextValue)
			.Select(SequenceColumns.BlockSize)
			.Where()
			.And(SequenceColumns.Name,ss.name);
			
		query.Execute();
		
		if(query.FetchNextRow())
		{
			ss.version
				= ((SmallintShortOutputColumnMap)query.GetColumnMap(SequenceColumns.Version)).GetValue();
			
			ss.blockSize
				= ((SmallintShortOutputColumnMap)query.GetColumnMap(SequenceColumns.BlockSize)).GetValue();
			
			ss.nextValue
				= ((BigintLongOutputColumnMap)query.GetColumnMap(SequenceColumns.NextValue)).GetValue();
			
			return true;
		}
		else
		{
			return false;
		}
	}

	private static class SequenceState
	{
		public SequenceState(String name)
		{
			nextValue = 1000;
			blockSize = 500;
			this.name = name;
			version = 0;
		}
		
		public boolean AtEnd()
		{
			if(current < 0)
			{
				current = nextValue;
			}
			return !(current < (nextValue + blockSize));
		}
		
		public long Increment()
		{
			if(current < 0)
			{
				current = nextValue;
			}
			return current++;
		}
		
		public long nextValue;
		public long blockSize;
		public String name;
		public short version;
		
		private long current = -1;
	}

	private static enum SequenceColumns
		implements TableColumn
	{
		Name,Version,NextValue,BlockSize;
		
		@Override
		public PrintWriter Generate(PrintWriter out) throws IOException
		{
			out.print(GetName());
			return out;
		}

		public static final Table TABLE = new Table("Sequence");
		
		@Override
		public Table GetTable()
		{
			return TABLE;
		}
		@Override
		public DBOutputColumnMap GetDBOutputColumnMap()
		{
			switch(this)
			{
				case Name:
					return IOCContainer.Lookup(DBMapUtil.class).GetOutputColumnMap(DBType.CHAR,String.class);
	 			case Version:
					return IOCContainer.Lookup(DBMapUtil.class).GetOutputColumnMap(DBType.SMALLINT,Short.TYPE);
	 			case NextValue:
					return IOCContainer.Lookup(DBMapUtil.class).GetOutputColumnMap(DBType.BIGINT,Long.TYPE);
	 			case BlockSize:
					return IOCContainer.Lookup(DBMapUtil.class).GetOutputColumnMap(DBType.SMALLINT,Short.TYPE);
				default:
					throw new RuntimeException("Impossible! Non existent enum value! Perhaps some build/classpath glitch");
			}
		}

		@Override
		public String GetName()
		{
			switch(this)
			{
				case Name:
					return "name";
	 			case Version:
					return "version";
	 			case NextValue:
					return "nextValue";
	 			case BlockSize:
					return "blockSize";
				default:
					throw new RuntimeException("Impossible! Non existent enum value! Perhaps some build/classpath glitch");
			}
		}

		@Override
		public String GetQualifiedName()
		{
			return GetName();
		}
	}
	
	private HashMap<String,SequenceState> sequenceByName = new HashMap<String, SequenceState>();
}
